/*
 * Copyright 2011 Romain Gilles
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jvdb;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.Ref;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * TODO document it
 * TODO implement logback style. by replacing ? by {} it must be tested vs performance
 * TODO implement batched prepare statement param parsing.
 * @author Romain Gilles
 */
class PreparedStatementTimer extends AbstractPreparedStatementTimer {
    private static final char PARAMETER_CHAR = '?';
    static final String PARAMETER_STRING = String.valueOf(PARAMETER_CHAR);

    private static final Logger logger = LoggerFactory.getLogger(STATEMENT_LOGGER_NAME + ".prd");
    private final List<List<Converter>> batchedParams = new ArrayList<List<Converter>>();

    private final List<Converter> params = newParams();

    private ArrayList<Converter> newParams() {
        return new ArrayList<Converter>();
    }


    static PreparedStatementTimer newInstance(int connectionId, int statementId, String sql) {
        return new PreparedStatementTimer(connectionId, statementId, sql);
    }

    PreparedStatementTimer(int connectionId, int statementId, String slq) {
        super(connectionId, statementId, slq);
    }

    @Override
    public void addBatch() {
        super.addBatch();
        batchedParams.add(new ArrayList<Converter>(params));
        params.clear();
    }

    @Override
    String parseSql(String sql) {
        if (params.isEmpty()) {
            return sql;
        }
        StringBuilder result = new StringBuilder(1024);
        buildStatement(sql, result, splitStatement(sql), params);
        return result.toString();
    }

    private void buildStatement(String sql, StringBuilder result, String[] splitStatement, List<Converter> params) {
        result.append(splitStatement[0]);
        int i = 1;
        for (; i < splitStatement.length; i++) {
            result.append(getParameter(params, i));
            result.append(splitStatement[i]);
        }
        if (sql.endsWith(PARAMETER_STRING)) {
            result.append(getParameter(i));
        }
    }

    private String[] splitStatement(String sql) {
        return sql.split("\\?");
    }

    @Override
    String doParseSql(List<String> statements) {
        if (statements.size() != batchedParams.size()) {
            return super.parseSql(statements)
                    + String.format(" /* error in batched prepared statement %d statements for %d params rows */ "
                                        ,statements.size(),batchedParams.size());
        }
        StringBuilder result = newStatementBuffer(batchedParams.size());
        String[] splitStatement = splitStatement(sql);
        for (List<Converter> params : batchedParams) {
            buildStatement(sql, result, splitStatement, params);
            result.append(STATEMENT_END);
        }
        return result.toString();
    }

    protected String getParameter(int i) {
        List<Converter> params = this.params;
        return getParameter(params, i);
    }

    private String getParameter(List<Converter> params, int i) {
        if (i < params.size()) {
            return params.get(i).convert();
        }
        return PARAMETER_STRING;
    }

    private void setParameter(int parameterIndex, Converter value) {
        while (parameterIndex >= params.size()) {
            params.add(params.size(), UnknownConverter.getInstance());
        }
        params.set(parameterIndex, value);
    }

    public void setNull(int parameterIndex, int sqlType) {
        setParameter(parameterIndex, NullConverter.getInstance());
    }

    public void setBoolean(int parameterIndex, boolean x) {
        setParameter(parameterIndex, BooleanConverter.getInstance(x));
    }

    public void setByte(int parameterIndex, byte x) {
        setParameter(parameterIndex, new SimpleConverter((Object) x));
    }

    public void setShort(int parameterIndex, short x) {
        setParameter(parameterIndex, new SimpleConverter((Object) x));
    }

    public void setInt(int parameterIndex, int x) {
        setParameter(parameterIndex, new SimpleConverter(x));
    }

    public void setLong(int parameterIndex, long x) {
        setParameter(parameterIndex, new SimpleConverter((Object) x));
    }

    public void setFloat(int parameterIndex, float x) {
        setParameter(parameterIndex, new SimpleConverter(x));
    }

    public void setDouble(int parameterIndex, double x) {
        setParameter(parameterIndex, new SimpleConverter((Object) x));
    }

    public void setBigDecimal(int parameterIndex, BigDecimal x) {
        setParameter(parameterIndex, new SimpleConverter((Object) x));
    }

    public void setString(int parameterIndex, String x) {
        setParameter(parameterIndex, new StringConverter(x));
    }

    public void setBytes(int parameterIndex, byte[] x) {
        setParameter(parameterIndex, new SimpleConverter((Object) x));
    }

    public void setDate(int parameterIndex, Date x) {
        setParameter(parameterIndex, DateConverter.newInstance(x));
    }

    public void setTime(int parameterIndex, Time x) {
        setParameter(parameterIndex, new SimpleConverter(x));
    }

    public void setTimestamp(int parameterIndex, Timestamp x) {
        setParameter(parameterIndex, TimestampConverter.newInstance(x));
    }

    public void setAsciiStream(int parameterIndex, InputStream x, int length) {
        // TODO support it
        logger.debug("call setCharacterStream not yet supported");
    }

    public void setUnicodeStream(int parameterIndex, InputStream x, int length) {
        // TODO support it
        logger.debug("call setCharacterStream not yet supported");
    }

    public void setBinaryStream(int parameterIndex, InputStream x, int length) {
        // TODO support it
        logger.debug("call setCharacterStream not yet supported");
    }

    public void setObject(int parameterIndex, Object x, int targetSqlType,
                          int scale) {
        setObject(parameterIndex, x);
    }

    public void setObject(int parameterIndex, Object x, int targetSqlType) {
        setObject(parameterIndex, x);
    }

    public void setObject(int parameterIndex, Object x) {
        if (x instanceof Date) {
            setParameter(parameterIndex, DateConverter.newInstance((Date) x));
        } else {
            setParameter(parameterIndex, new SimpleConverter(x));
        }
    }

    public void setCharacterStream(int parameterIndex, Reader reader, int length) {
        // TODO support it
        logger.debug("call setCharacterStream not yet supported");
    }

    public void setRef(int i, Ref x) {
        // TODO support it
        logger.debug("call setRef not yet supported");
    }

    public void setBlob(int i, Blob x) {
        // TODO support it
        logger.debug("call setBlob not yet supported");
    }

    public void setClob(int i, Clob x) {
        // TODO support it
        logger.debug("call setClob not yet supported");
    }

    public void setArray(int i, Array x) {
        // TODO support it
        logger.debug("call setArray not yet supported");
    }

    public void setDate(int parameterIndex, Date x, Calendar cal) {
        setParameter(parameterIndex, DateConverter.newInstance(x, cal));
    }

    public void setTime(int parameterIndex, Time x, Calendar cal) {
        setObject(parameterIndex, x);
    }

    public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) {
        setParameter(parameterIndex, TimestampConverter.newInstance(x, cal));
    }

    public void setNull(int paramIndex, int sqlType, String typeName) {
        setNull(paramIndex, sqlType);
    }

    public void setURL(int parameterIndex, URL x) {
        setObject(parameterIndex, x);
    }
}
